TerraformでLambda[Python]のデプロイするときのプラクティス
※先に断っておきますがベストプラクティスではないです。
訳あって最近は、Lambda FunctionとLayerをTerraformでデプロイしています。
CloudFormationと比べてると自動ロールバック機能はないのですが、デプロイが早く気に入っています。
ただ、いくつかハマりポイントがあったので、今回はそこらへんの知見を紹介したいともいます。
せっかちな人へ
GitHubにソースコードあげています。
概要
ファイル構成
$ tree -L 2 . ├── README.md ├── main.tf ├── src │ └── get_unixtime.py ├── requirements.txt ├── build-lambda.sh └── build
main.tf
- Terraformのtfファイル
src/get_unixtime.py
- Lambda Function用のコード
requirements.txt
- 依存ライブラリ
build-lambda.sh
- ビルドスクリプト
build
- ソースコードとライブラリのビルド後の一時ファイル置き場
Pythonのソースコード
UNIXTIMEのタイムスタンプを返却するシンプルなコードです。pytz
ライブラリをつかっています。
import os import sys import pytz from datetime import datetime, timedelta def lambda_handler(event: dict, context): UTC = pytz.timezone('UTC') now = datetime.now(UTC) timestamp = now.timestamp() return timestamp
Terraformのtfファイル
Lambda Function用とLambda Layer用のZipファイルをそれぞれ作成しています。
source_code_hash
を使っているので差分が発生しない限りデプロイされないという作りになっています。
差分とは、
- ソースコードが変化した
- ライブラリが変化した(バージョンの更新など)
のどちらかを指します。
# Terraform Setting terraform { required_version = "0.12.6" } # Provider provider "aws" { region = "ap-northeast-1" version = "~>2.34.0" } # Variables variable "system_name" { default="terraform-lambda-deployment" } # Archive data "archive_file" "layer_zip" { type = "zip" source_dir = "build/layer" output_path = "lambda/layer.zip" } data "archive_file" "function_zip" { type = "zip" source_dir = "build/function" output_path = "lambda/function.zip" } # Layer resource "aws_lambda_layer_version" "lambda_layer" { layer_name = "${var.system_name}_lambda_layer" filename = "${data.archive_file.layer_zip.output_path}" source_code_hash = "${data.archive_file.layer_zip.output_base64sha256}" } # Function resource "aws_lambda_function" "get_unixtime" { function_name = "${var.system_name}_get_unixtime" handler = "src/get_unixtime.lambda_handler" filename = "${data.archive_file.function_zip.output_path}" runtime = "python3.6" role = "${aws_iam_role.lambda_iam_role.arn}" source_code_hash = "${data.archive_file.function_zip.output_base64sha256}" layers = ["${aws_lambda_layer_version.lambda_layer.arn}"] } # Role resource "aws_iam_role" "lambda_iam_role" { name = "${var.system_name}_iam_role" assume_role_policy = <<POLICY { "Version": "2012-10-17", "Statement": [ { "Action": "sts:AssumeRole", "Principal": { "Service": "lambda.amazonaws.com" }, "Effect": "Allow", "Sid": "" } ] } POLICY } # Policy resource "aws_iam_role_policy" "lambda_access_policy" { name = "${var.system_name}_lambda_access_policy" role = "${aws_iam_role.lambda_iam_role.id}" policy = <<POLICY { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:CreateLogStream", "logs:CreateLogGroup", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:*:*:*" } ] } POLICY }
ビルド
今回の肝となるビルドスクリプトです。
ここでは
- Lambda Function用のソースコードをコピー
- Lambda Layer用のデプロイに含めるライブラリをインストール
しています。
※詳しい処理は後半で説明します。内容を踏まえた上で、利用を検討してください。
#!/usr/bin/env bash if [ -d build ]; then rm -rf build fi # Recreate build directory mkdir -p build/function/ build/layer/ # Copy source files echo "Copy source files" cp -r src build/function/ # Pack python libraries echo "Pack python libraries" pip install -r requirements.txt -t build/layer/python # Remove pycache in build directory find build -type f | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm
デプロイ
下記の手順でデプロイできます。
# ビルドスクリプトを実行 sh lambda-build.sh # Terraformのデプロイコマンドを実行 terraform apply
ビルドスクリプトの解説
ここでは、ビルドスクリプトbuild-lambda.sh
でおこなっている内容について詳しく説明します。
ソースファイルのコピー
cp -r src build/function/
の部分です。Lambda Functionのデプロイパッケージに含めるソースコードをbuild
ディレクトリに配置しています。
ファイルのコピーをしているだけなので、一見必要無さそうに見えます。
しかし、仮にtfファイルのarchive_file
の部分を
data "archive_file" "function_zip" { type = "zip" source_dir = "src" ...省略
みたいに書き換えると、Lambdaパッケージにはsrc
部分が含まれず、src直下のディレクトリからデプロイパッケージが作成されます。
つまり、handlerや他のファイルとのパスの関係から、このような構成にしています。
ライブラリ群のインストール
pip install -r requirements.txt -t build/layer/python
の部分です。Lambda Layerのデプロイパッケージに含めるライブラリ一式をbuild
ディレクトリに配置しています。
ポイントはpython
配下にライブラリ群を一式インストールしている点です。Lambda FunctionからLambda Layerを参照するパスをうまく通すためこのような構成にしています。
キャッシュ(pycache)の削除
ここが今回の一番のポイントです。find build -type f | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm
の部分ですが、ここではインストールしたライブラリに含まれる__pycache__
配下のファイルを一式削除しています。
[追記 2020/02/26] こちら をよく見てみると、pycleanというDebianパッケージがあるらしいです。これをベースにPipyでpycleanというライブラリを提供している人がいまいした。こちらを利用した方が良さそうです。
[さらに追記 2020/03/30] 前回追記したpycleanというライブラリですが、どうやらOSのパッケージを参照しているらしく、環境依存になるため使えませんでした。
これは、pip installで同じライブラリをインストールする際でも__pycache__
配下のファイルの影響で差分が発生してしまうからです。
たとえば、2回目以降のデプロイで__pycache__
配下のファイルを削除をせずsh lambda-build.sh
を実行したとします。
その後terrform plan
を実行すると、source_code_hash
のハッシュ値が一致しせず差分が検知されます。
試しに、
pip install -r requirements.txt -t lib1 pip install -r requirements.txt -t lib2
を実行してdiff
をとってみましたが、__pycache__
配下がすべて差分として出力されました。
$ diff -r lib1/ lib2/ Binary files lib1/pytz/__pycache__/exceptions.cpython-36.pyc and lib2/pytz/__pycache__/exceptions.cpython-36.pyc differ Binary files lib1/pytz/__pycache__/__init__.cpython-36.pyc and lib2/pytz/__pycache__/__init__.cpython-36.pyc differ Binary files lib1/pytz/__pycache__/lazy.cpython-36.pyc and lib2/pytz/__pycache__/lazy.cpython-36.pyc differ Binary files lib1/pytz/__pycache__/reference.cpython-36.pyc and lib2/pytz/__pycache__/reference.cpython-36.pyc differ Binary files lib1/pytz/__pycache__/tzfile.cpython-36.pyc and lib2/pytz/__pycache__/tzfile.cpython-36.pyc differ Binary files lib1/pytz/__pycache__/tzinfo.cpython-36.pyc and lib2/pytz/__pycache__/tzinfo.cpython-36.pyc differ
これを回避するため、下記の方法を調べてみましたが、良さそうながありませんでした。。。
- Terraformのビルドイン関数でなんか使えそうなものはないか? => なさそう
- Terraformでzip化する際にexclude指定できないか? => なさそう
- pip install時に
__pycache__
の作成が行われないようなオプションはないか? =>export PYTHONDONTWRITEBYTECODE=1
やpython -B -m pip install
を試してみましたが、pip install
したファイルへの影響しない
いろいろ調べた結果、あくまでキャッシュということで消しても問題なさそうなため、インストール後に一括で削除することにしました。デプロイして動作確認はしましたが、問題なく動きます。
余談
- そもそも
pycache
とはなんなのか?
公式ドキュメントを確認しましたが、__pycache__
配下に作成されるファイルは、Pythonの実行時に生成されるコンパイルされたモジュール群のキャッシュだそうです。
.pyc
を残しておけば、モジュールの読み込みにかかる時間が短縮されるので、全体の実行時間が速くなるらしいです。ただ、プログラムを変更するたびに書き換えられます。
GitなどでPythonのコードを管理する際は、.gitignore
に追記して除外するのが一般的です。
今回のケースの場合で言えば、(ともとも除外していたものなので)削除しても影響無さそうに思えますが、pip installしたライブラリへの影響まではわからないのが正直なところです。
まとめ
いかがだったでしょうか。
繰り返しになりますが、この方法でデプロイする際は十分に検討してからおこなってください。
また、他にいい方法知っているという方いれば是非おしてください。